/*
 * Galaxium Messenger
 * 
 * Copyright (C) 2007 Ben Motmans <ben.motmans@gmail.com>
 * Copyright (C) 2009 Philippe Durand <draekz@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.Text;
using System.Collections.Generic;
using System.Reflection;

using Anculus.Core;
using Galaxium.Core;
using Galaxium.Client;
using Galaxium.Protocol;

namespace Galaxium.Protocol.Irc
{
	public struct ChannelListEntry
	{
		public string Name;
		public int Visible;
		public string Topic;
	}
	
	public class IrcConnection : TCPConnection
	{
		public event EventHandler<ServerEventArgs> MessageReceived;
		
		private MessageBuffer _buffer;
		private IrcContact _serverContact;
		private bool _isLoggedIn = false;
		
		public bool IsLoggedIn
		{
			get { return _isLoggedIn; }
		}
		
		//TODO: the session contact add/remove events --> use IEntity for the group, so irc can pass the channel
		//FIXME: when somebody sends us a PRIVMSG or NOTICE in a non-existing conversation, the first message isn't shown in the gui
		//   --> we need to store the message temporarily and add it to the textview when initialized
		public IrcConnection (IrcSession session, IrcConnectionInfo connectionInfo) : base (session, connectionInfo)
		{
			_buffer = new MessageBuffer (this);
		}
		
		public override void Disconnect ()
		{
			SendCleanDisconnect ();
			
			base.Disconnect ();
		}
		
		protected internal void SendCleanDisconnect ()
		{
			StringBuilder sb = new StringBuilder ();
			sb.Append (IrcConstants.Commands.Quit);
			sb.Append (' ');
			sb.Append (ClientUtility.ClientName);
			sb.Append (' ');
			sb.Append (ClientUtility.ClientVersion);
			sb.Append (" -- ");
			sb.Append (ClientUtility.ClientWebsite);
			
			Send (sb);
		}
		
		protected override void OnEstablished (ConnectionEventArgs args)
		{
			IrcAccount account = (Session as IrcSession).Account as IrcAccount;
			if (!String.IsNullOrEmpty (account.Password))
				Send (String.Concat (IrcConstants.Commands.Password, " ", account.Password));
			
			Send ("NICK " + account.DisplayName);

			StringBuilder sb = new StringBuilder ();
			sb.Append (IrcConstants.Commands.User);
			sb.Append (' ');
			sb.Append (account.DisplayName);
			sb.Append (" 0 * :");
			sb.Append (account.DisplayName);
			
			Send (sb);
			
			base.OnEstablished (args);
		}
		
		protected override void OnDataReceived (ConnectionDataEventArgs args)
		{
			_buffer.AppendData (args.Buffer, args.Length);
			
			while (_buffer.IsMessageAvailable ())
			{
				IrcMessage msg = _buffer.PopMessage ();
				
				switch (msg.Command)
				{
					// Command replies for admin info from server
					case IrcConstants.Replies.ReplyAdminMe:
					case IrcConstants.Replies.ReplyAdminLoc1:
					case IrcConstants.Replies.ReplyAdminLoc2:
					case IrcConstants.Replies.ReplyAdminMail:
						OnMessageReceived(msg, msg.LastParameter);
						break;
					
					// Command replies for server information
					case IrcConstants.Replies.ReplyInfo:
						OnMessageReceived(msg, msg.LastParameter);
						break;
					case IrcConstants.Replies.ReplyEndOfInfo:
						break;
					
					// Replies that give us channel ban info
					case IrcConstants.Replies.ReplyBanList:
						HandleBanList(msg);
						break;
					case IrcConstants.Replies.ReplyEndOfBanList:
						HandleEndBanList (msg);
						break;
						
					case IrcConstants.Replies.ReplyInviting:
						// Reply if we successfully invited someone.
						break;
						
					// Command dealing with private messages and DCC requests
					case IrcConstants.Commands.PrivateMessage:
						HandlePrivateMessage (msg);
						break;
					
					// Commands dealing with channels
					case IrcConstants.Commands.Join:
						HandleJoin (msg);
						break;
					case IrcConstants.Commands.Part:
						HandlePart (msg);
						break;
					case IrcConstants.Commands.Quit:
						HandleQuit (msg);
						break;
					case IrcConstants.Commands.Mode:
						HandleMode (msg);
						break;
					
					case IrcConstants.Commands.Notice:
						HandleNotice (msg);
						break;
					case IrcConstants.Commands.Kill:
						HandleKill (msg);
						break;
					case IrcConstants.Commands.Kick:
						HandleKick (msg);
						break;
					
					// Commands dealing with contact/self status
					case IrcConstants.Commands.Away:
						HandleAway (msg);
						break;
					case IrcConstants.Replies.ReplyAway:
						HandleAwayReply (msg);
						break;
					case IrcConstants.Replies.ReplyNowAway:
						HandleNowAway (msg);
						break;
					case IrcConstants.Replies.ReplyUnAway:
						HandleUnAway (msg);
						break;
					
					case IrcConstants.Replies.ReplyUserHost:
						OnMessageReceived (msg, "Constact's Hostname: "+msg.LastParameter.Substring (msg.LastParameter.IndexOf ('@')+1));
						break;
						
					case IrcConstants.Replies.ReplyNoTopic:
						OnMessageReceived (msg, "Channel ["+msg.Parameters[1]+"] has no topic set.");
						break;
					
					case IrcConstants.Replies.ReplyVersion:
						OnMessageReceived (msg, msg.LastParameter);
						break;
						
					case IrcConstants.Commands.Invite:
						HandleInvite (msg);
						break;
					case IrcConstants.Commands.Nickname:
						HandleNickname (msg);
						break;
					case IrcConstants.Commands.Ping:
						HandlePing (msg);
						break;
					case IrcConstants.Commands.ServerQuit:
						HandleServerQuit (msg);
						break;
					case IrcConstants.Commands.Topic:
						HandleTopic (msg);
						break;
					case IrcConstants.Replies.ReplyTopic:
						HandleInitialTopic (msg);
						break;
						
					case IrcConstants.Replies.ReplyEndOfMotd:
						if (!_isLoggedIn)
						{
							_serverContact = new IrcContact (Session as IrcSession, msg.Identifier.Hostname, Session.Account.UniqueIdentifier);
							
							HandleConnectionEstablished ();
						}
						break;
						
					case IrcConstants.Replies.ErrorNoOperHost:
						OnMessageReceived (msg, "Unable to authorize account as an operator.");
						break;
						
					case IrcConstants.Replies.ErrorNoMotd:
						if (!_isLoggedIn)
						{
							_serverContact = new IrcContact (Session as IrcSession, msg.Identifier.Hostname, Session.Account.UniqueIdentifier);
							
							HandleConnectionEstablished ();
						}
						else
							OnMessageReceived(msg, "There is no MOTD available for ["+msg.Parameters[1]+"].");
						break;
					case IrcConstants.Replies.HeaderServerFlags:
						(Session as IrcSession).RemoteServerSettings.Parse (msg);
						break;
					case IrcConstants.Replies.ReplyNames:
						HandleNames (msg);
						break;
					case IrcConstants.Replies.ReplyEndOfNames:
						HandleEndOfNames (msg);
						break;
					case IrcConstants.Replies.ReplyChannelModeIs:
						HandleChannelModeIs (msg);
						break;
					case IrcConstants.Replies.ReplyUModeIs:
						// We should probably update our user mode (keep track of it).
						break;
						
					case IrcConstants.Replies.ErrorNoPrivileges:
						OnMessageReceived (msg, "Unable to use this command, you must be a IRC operator.");
						break;
						
					case IrcConstants.Replies.ErrorNicknameInUse:
						HandleNicknameInUse (msg);
						break;
					
					case IrcConstants.Replies.ErrorChannelOpPrivsNeeded:
						OnMessageReceived(msg, "You must be a channel operator in ["+msg.Parameters[1]+"] to do this.");
						break;
					
					case IrcConstants.Replies.ErrorCannotSendToChannel:
						HandleErrorCannotSendToChannel(msg);
						break;
					
					case IrcConstants.Replies.ReplyHeavyServerLoad: //263
						HandleHeavyServerLoad (msg);
						break;
					case IrcConstants.Commands.Error:
						HandleError (msg);
						break;
					case IrcConstants.Replies.ErrorNoSuchChannel:
						OnMessageReceived(msg, "Channel ["+msg.Parameters[1]+"] not found!");
						break;
					case IrcConstants.Replies.ErrorNoSuchNick:
						OnMessageReceived(msg, "Nickname ["+msg.Parameters[1]+"] not found!");
						break;
					case IrcConstants.Replies.ErrorNoSuchServer:
						OnMessageReceived(msg, "Server ["+msg.Parameters[1]+"] not found!");
						break;
					case IrcConstants.Replies.ErrorNotOnChannel:
						OnMessageReceived(msg, "You must already be in the ["+msg.Parameters[1]+"] to do that!");
						break;
					case IrcConstants.Replies.ErrorUserOnChannel:
						OnMessageReceived(msg, "Contact is in ["+msg.Parameters[2]+"] already!");
						break;
						
					case IrcConstants.Replies.ErrorBannedFromChannel:
						HandleBannedFromChannel (msg);
						break;
					case IrcConstants.Replies.ErrorInviteOnlyChannel:
						HandleInviteOnlyChannel (msg);
						break;
						
					case IrcConstants.Replies.ReplyMotd:
						HandleMotd (msg);
						break;
					case IrcConstants.Replies.ReplyMotdStart:
						break;
					case IrcConstants.Replies.HeaderWelcome:
					case IrcConstants.Replies.HeaderHost:
					case IrcConstants.Replies.HeaderServer:
					case IrcConstants.Replies.HeaderServerInfo:
					
					case IrcConstants.Replies.ReplyHighestConnectionCount: //250
					case IrcConstants.Replies.ReplyLUserClient: //251
					case IrcConstants.Replies.ReplyLUserOp: //252
					case IrcConstants.Replies.ReplyLUserUnknown: //253
					case IrcConstants.Replies.ReplyLUserChannels: //254
					case IrcConstants.Replies.ReplyLUserMe: //255
					case IrcConstants.Replies.ReplyCurrentLocalUsers: //265
					case IrcConstants.Replies.ReplyCurrentGlobalUsers: //266
						OnMessageReceived(msg, msg.LastParameter);
						break;
					case IrcConstants.Replies.ReplyChannelNicknameTime: //333
					case IrcConstants.Replies.ReplyChannelUrl: //328
					case IrcConstants.Replies.ReplyChannelTime: //329
						//ignore
						break;
					case IrcConstants.Replies.ReplyListStart: //321
						HandleStartOfChannels (msg);
						break;
					case IrcConstants.Replies.ReplyList: //322
						HandleChannels (msg);
						break;
					case IrcConstants.Replies.ReplyListEnd: //323
						HandleEndOfChannels (msg);
						break;
						
					default:
						Log.Warn ("No handler for command '{0}'", msg.Command);
						break;
				}
			}
			
			base.OnDataReceived (args);
		}
		
		private class MessageContainer
		{
			public IrcMessage Message;
			public string Output;
			
			public MessageContainer (IrcMessage msg, string output)
			{
				Message = msg;
				Output = output;
			}
		}
		
		private Queue<MessageContainer> _queuedMessages = new Queue<MessageContainer> ();
		
		public void OnMessageReceived (IrcMessage msg, string message)
		{
			if (MessageReceived != null)
			{
				if (_queuedMessages.Count > 0)
				{
					// If we do have a listener, lets pump out any pending messages.
					MessageContainer queued = _queuedMessages.Dequeue ();
					while (queued != null)
					{
						MessageReceived (this, new ServerEventArgs (queued.Message, queued.Output));
						
						if (_queuedMessages.Count > 0)
							queued = _queuedMessages.Dequeue ();
						else
							queued = null;
					}
				}
				
				MessageReceived (this, new ServerEventArgs (msg, message));
			}
			else
			{
				// If we dont have a listener somewhere, lets queue them.
				MessageContainer container = new MessageContainer (msg, message);
				_queuedMessages.Enqueue (container);
			}
		}
		
		protected internal void Send (string text)
		{
			Send (Encoding.UTF8.GetBytes (text + Environment.NewLine));
		}
		
		protected internal void Send (StringBuilder sb)
		{
			sb.Append (Environment.NewLine);
			Send (Encoding.UTF8.GetBytes (sb.ToString ()));
		}
		
		public void Send (IrcMessage msg)
		{
			ByteArray ba = new ByteArray ();
			//never send the ident, only used for incoming messages
			ba.Append (msg.Command);
			if (msg.HasParameters) {
				int len = msg.Parameters.Length;
				for (int i=0; i<len; i++) {
					string p = msg.Parameters[i];
					
					if (i == (len-1)) {
						//it's the last parameter, check if it contains a space
						//if so, prefix it with a semicolon
						if (p.Contains (" "))
							p = ":" + p;
					}
					
					ba.Append (' ');
					ba.Append (p);
				}
			}
			
			ba.Append (Environment.NewLine);
			
			Send (ba.GetByteArray ());
		}
		
		internal void Pong (string reply)
		{
			ThrowUtility.ThrowIfEmpty ("reply", reply);
			
			Send (String.Concat (IrcConstants.Commands.Pong, " ", reply));
		}
		
		protected internal void HandlePrivateMessage (IrcMessage msg)
		{
			//somebody is talking in a channel or directly to us
			//:<contact> PRIVMSG <target:channel or me> :<text> 
			string target = msg[0];
			
			IrcSession session = _session as IrcSession;
			IrcContact contact = null;
			IrcConversation conversation = null;
			
			if (target.ToLower() == _session.Account.DisplayName.ToLower())
			{
				if (msg[1][0] == IrcConstants.Markers.Ctcp)
				{
					//this is a DCC or CTCP message
					string datastr = msg[1].Substring (1, msg[1].Length - 2);
					
					if (datastr.ToUpper ().StartsWith ("DCC"))
						HandleDcc (msg.Identifier, datastr);
					else
						HandleCtcpRequest (msg.Identifier, datastr);
					
					return;
				}
				else
				{
					// Since a private message is not related to a specific channel,
					// lets obtain from the session.
					contact = session.ObtainContact (msg.Identifier.Nickname, msg.Identifier.Hostname);
					
					// Obtain a conversation object that we've used before, or new.
					conversation = session.ObtainConversation (contact, true);
				}
			}
			else
			{
				// somebody is talking in a channel
				IrcChannel channel = session.ObtainChannel (target, true);
				conversation = session.ObtainConversation (channel, true);
				contact = conversation.GetContact (msg.Identifier.Nickname);
			}
			
			conversation.EmitMessageReceived (contact, msg[1]);
		}
		
		protected internal void HandleJoin (IrcMessage msg)
		{
			//somebody joined a channel
			//:<contact> JOIN <channel>
			IrcSession session = Session as IrcSession;
			IrcChannel channel = session.ObtainChannel (msg[0], true);
			IrcConversation conv = session.ObtainConversation (channel, true);
			
			// check the conversation for an existing contact, we dont want
			// to add them twice (like the channel creator, or initial join.
			IrcContact contact = conv.GetContact (msg.Identifier.Nickname);
			
			if (contact == null)
			{
				// Here we need to create a new contact, because this one will
				// have its own seperate channel modes. We wouldn't want the same
				// contact to show up as an operator in another channel.
				contact = new IrcContact (session, msg.Identifier.Hostname, msg.Identifier.Nickname);
				contact.Conversation = conv;
				//contact.ChannelUserMode = ChannelModeFlags.None;
				
				conv.AddContact (contact); //this also emits an event
			}
			else
			{
				contact.ChannelUserMode = ChannelModeFlags.None;
			}
			
			// Lets not fire off the event for our own account.
			if (contact.DisplayName.CompareTo(Session.Account.DisplayName) == 0)
			{
				conv.IsJoined = true;
				OnMessageReceived (msg, "You have joined the ["+msg.Parameters[0]+"] channel.");
			}
			
			// We should notify the contact list as well
			session.EmitContactChanged (new ContactEventArgs (channel));
			
			// This event does the adding of the contact to the conversation's little list.
			conv.EmitChannelContactJoin (new ChannelContactEventArgs (channel, contact));
		}
		
		protected internal void HandlePart (IrcMessage msg)
		{
			//somebody left a channel
			//:<contact> PART <channel>
			IrcSession session = Session as IrcSession;
			IrcChannel channel = session.ObtainChannel (msg[0], true);
			IrcConversation conv = session.ObtainConversation (channel, true);
			
			if (conv == null)
			{
				OnMessageReceived (msg, "You have parted from the ["+msg.Parameters[0]+"] channel without a conversation.");
				return;
			}
			
			//<contact>=<nickname>!<ident>
			IContact contact = conv.GetContact (msg.Identifier.Nickname);
			conv.RemoveContact (contact);
			
			// Lets not fire off the event for our own account.
			if (contact.DisplayName.CompareTo(Session.Account.DisplayName) == 0)
			{
				conv.IsJoined = false;
				OnMessageReceived (msg, "You have parted from the ["+msg.Parameters[0]+"] channel.");
			}
			
			// We should notify the contact list as well
			session.EmitContactChanged (new ContactEventArgs (channel));
			
			conv.EmitChannelContactPart (new ChannelContactEventArgs (channel, contact));
		}
		
		protected internal void HandleQuit (IrcMessage msg)
		{
			//somebody quit
			//:<contact> QUIT :<message>
			IrcSession session = Session as IrcSession;
			
			foreach (IContact cycle in Session.ContactCollection)
			{
				IrcChannel channel = cycle as IrcChannel;
				
				if (channel == null)
					continue;
				
				if (channel.Presence == IrcPresence.Offline)
					continue;
				
				IrcConversation conversation = session.ObtainConversation (channel, true);
				IContact contact = channel.ContactCollection.GetContactByName (msg.Identifier.Nickname);
				
				if (contact != null)
				{
					conversation.RemoveContact (contact);
					conversation.EmitChannelContactQuit (new ChannelContactMessageEventArgs (channel, contact, msg[0]));
				}
			}
		}
		
		protected internal void HandleMode (IrcMessage msg)
		{
			IrcSession session = _session as IrcSession;
			string target = msg[0];
			IrcContact actor = null;
			
			if (IrcProtocolHelper.IsValidChannelName (session.RemoteServerSettings, target))
			{
				//:<server> MODE <channel> <mode> <nickname(s)/arg(s)>
				
				bool give = false;
				ChannelModeFlags[] flags;
				
				// Now we either get the channel object we had, or create a new one.
				IrcChannel channel = session.ObtainChannel (msg[0], true);
				
				// Now we do the same for the conversation object.
				IrcConversation conv = session.Conversations.GetConversation (channel) as IrcConversation;
				
				// Find who the contact is that is causing the mode change.
				if (msg.Identifier.IsServerIdentifier)
					actor = _serverContact;
				else
					actor = channel.ContactCollection.GetContactByName (msg.Identifier.Nickname) as IrcContact;
				
				// Parse the channel mode flags that have been passed.
				if (IrcProtocolHelper.ParseChannelModeFlags (msg[1], out give, out flags))
				{
					int nextArgument = 2;
					
					// Go through all the flags that are being set/unset
					foreach (ChannelModeFlags flag in flags)
					{
						switch (flag)
						{
							case ChannelModeFlags.Operator:
							case ChannelModeFlags.Voice:
							case ChannelModeFlags.HalfOperator:
							case ChannelModeFlags.ChannelCreator:
								// Go through all the contacts that have been specified.
								for(int i = nextArgument; i < msg.Parameters.Length; i++)
								{
									if (String.IsNullOrEmpty(msg[i]))
										break;
									
									IrcContact victim = channel.ContactCollection.GetContactByName(msg[i]) as IrcContact;
									
									if (victim != null)
									{
										ChannelContactModeEventArgs contactArgs = new ChannelContactModeEventArgs (channel, actor, victim, (give ? "+":"-") + IrcProtocolHelper.GetChannelModeString (flag));
										
										// Actually apply the flag changes to the contact.
										if (give)
											victim.ChannelUserMode |= flag;
										else
											victim.ChannelUserMode &= ~flag;
										
										// Notify a conversation of this change.
										if (conv != null)
											conv.EmitChannelContactModeChanged (contactArgs);
									}
									else
									{
										Log.Warn ("A mode command issued for unknown contact: "+msg[i]);
									}
								}
								break;
							
							case ChannelModeFlags.UserLimit: // next param is the limit to set
								break;
							
							case ChannelModeFlags.BanException: // next param is a ban exception
								break;
							
							case ChannelModeFlags.BanMask: // next param is a ban mask
								break;
							
							case ChannelModeFlags.ChannelKey: // next param is the password
								break;
							
							default:
								ChannelModeEventArgs args = new ChannelModeEventArgs (channel, actor, flag, give, null);
								
								if (give)
									channel.ChannelFlags |= flag;
								else
									channel.ChannelFlags &= ~flag;
								
								channel.EmitChannelModeChanged (args);
								break;
						}
					}
				}
				else
				{
					Log.Warn ("No flags were found during parse!");
				}
			}
			else
			{
				//:<server> MODE <contact> <mode>
				
				IrcAccount account = session.Account as IrcAccount;
				UserModeFlags[] flags;
				bool give = false;
				
				// If we are getting mode changes for ourselves, we wont be in the collection
				if (msg[0].ToLower().CompareTo (account.DisplayName.ToLower()) == 0)
				{
					// We are setting mode for ourselves!
					if (IrcProtocolHelper.ParseUserModeFlags (msg[1], out give, out flags))
					{
						// We have the flags
						foreach (UserModeFlags flag in flags)
						{
							if (give)
								account.UserMode |= flag;
							else
								account.UserMode &= ~flag;
						}
					}
				}
				else
				{
					// This is for a contact
					IrcContact victim = session.ObtainContact (msg[0], msg[0]) as IrcContact;
					
					if (IrcProtocolHelper.ParseUserModeFlags (msg[1], out give, out flags))
					{
						// We have the flags
						foreach (UserModeFlags flag in flags)
						{
							if (give)
								victim.UserMode |= flag;
							else
								victim.UserMode &= ~flag;
							
							UserModeEventArgs args = new UserModeEventArgs (victim, flag, give);
							session.EmitUserModeChanged (args);
						}
					}
				}
			}
		}
		
		protected internal void HandleNotice (IrcMessage msg)
		{
			//a notice was send to us
			//:<contact> NOTICE <target> :<message>
			
			string target = msg[0];
			
			IrcSession session = _session as IrcSession;
			IrcContact contact = null;
			IrcConversation conversation = null;

			if (!msg.HasIdentifier) {
				//for example, a notice message during connection
				//eg: NOTICE AUTH :*** Looking up your hostname
				OnMessageReceived(msg, msg.LastParameter);
				return;
			}

			if (target == _session.Account.DisplayName) {
				if (msg[1][0] == IrcConstants.Markers.Ctcp) {
					//this is a CTCP message
					string datastr = msg[1].Substring (1, msg[1].Length - 2);
					HandleCtcpResponse (msg.Identifier, datastr);
					
					return;
				} else if (msg.Identifier.IsServerIdentifier) {
					//TODO: this notice belongs in the server messages
					return;
				} else {
					//private notice
					contact = session.ObtainContact (msg.Identifier.Nickname, msg.Identifier.Hostname);
					if (contact == null) {
						//this is a new conversation, so the contact doesn't exist yet
						contact = new IrcContact (session, null, msg.Identifier.Hostname, msg.Identifier.Nickname);
						//TODO: store the contact
					}
					conversation = session.ObtainConversation (contact, true); //the new conversation is automatically stored in the contact
				}
			} else {
				//channel notice
				IrcChannel channel = session.ObtainChannel (target, true);
				conversation = session.ObtainConversation (channel, true);
				contact = conversation.GetContact (msg.Identifier.Nickname);
			}

			conversation.EmitNoticeReceived (contact, msg[1]);
		}
		
		protected internal void HandleKill (IrcMessage msg)
		{
			//somebody was killed from the server
			//:<contact> KILL <nickname> :<reason>
			
			IrcSession session = Session as IrcSession;
			foreach (IrcChannel channel in Session.ContactCollection) {
				if (channel.Presence == IrcPresence.Offline)
					continue;

				IrcConversation conversation = session.ObtainConversation (channel, true);
				IContact contact = channel.ContactCollection.GetContactByName (msg[0]);
				
				if (contact != null) {
					conversation.RemoveContact (contact);
					conversation.EmitChannelContactKill (new ChannelContactActionEventArgs (channel, msg.Identifier.Nickname, contact, msg[1]));
				}
			}
		}
		
		protected internal void HandleKick (IrcMessage msg)
		{
			//somebody was kicked from a channel
			//:<contact> KICK <channel> <nickname> :<reason>
			
			IrcSession session = Session as IrcSession;
			IrcChannel channel = session.ObtainChannel (msg[0], true);
			IrcConversation conversation = session.ObtainConversation (channel, true);

			IContact contact = channel.ContactCollection.GetContactByName (msg[1]);
				
			if (contact != null)
			{
				// Lets not fire off the event for our own account.
				if (contact.DisplayName.CompareTo(Session.Account.DisplayName) == 0)
				{
					conversation.IsJoined = false;
					OnMessageReceived (msg, "You have parted from the ["+msg.Parameters[0]+"] channel.");
				}
				
				// We should notify the contact list as well
				session.EmitContactChanged (new ContactEventArgs (channel));
				
				conversation.RemoveContact (contact);
				conversation.EmitChannelContactKick (new ChannelContactActionEventArgs (channel, msg.Identifier.Nickname, contact, msg[1]));
			}
		}
		
		protected internal void HandleAway (IrcMessage msg)
		{
			//somebody changed his/her status to away or is back
			//:<contact> AWAY <nickname> :<message>
			
			IPresence presence = null;
			if (msg.Parameters.Length == 1)
				presence = IrcPresence.Online;
			else
				presence = new AwayPresence (msg[1]);

			foreach (IrcChannel channel in Session.ContactCollection)
			{
				if (channel.Presence == IrcPresence.Offline)
					continue;

				IContact contact = channel.ContactCollection.GetContactByName (msg[0]);
				if (contact != null)
					contact.Presence = presence;
			}
		}
		
		protected internal void HandleAwayReply (IrcMessage msg)
		{
			OnMessageReceived(msg, msg.LastParameter);
		}
		
		protected internal void HandleNowAway (IrcMessage msg)
		{
			OnMessageReceived(msg, "You are now marked as being away.");
			(Session.Account as IrcAccount).SetPresence(IrcPresence.Away);
			
		}
		
		protected internal void HandleUnAway (IrcMessage msg)
		{
			OnMessageReceived(msg, "You are no longer marked as being away.");
			
			(Session.Account as IrcAccount).SetPresence(IrcPresence.Online);
		}
		
		protected internal void HandleInvite (IrcMessage msg)
		{
			OnMessageReceived(msg, "You have been invited to join ["+msg.Parameters[1]+"] by ["+msg.Identifier.Nickname+"].");
			//somebody invited us into a channel
			//TODO: raise an event --> can we use the "ConversationRequested" event ?
		}
		
		
		
		protected internal void HandleStartOfChannels (IrcMessage msg)
		{
			(Session as IrcSession).ChannelList.Clear();
		}
		
		protected internal void HandleChannels (IrcMessage msg)
		{
			if (msg.Parameters.Length >= 4)
			{
				string name = msg[1];
				int visible = int.Parse (msg[2]);
				string topic = String.Empty;
				if (!String.IsNullOrEmpty (msg[3]))
					topic = msg[3];
				
				ChannelListEntry entry = new ChannelListEntry ();
				entry.Name = name;
				entry.Visible = visible;
				entry.Topic = msg.LastParameter;
				
				(Session as IrcSession).ChannelList.Add (entry);
			}
		}
		
		protected internal void HandleEndOfChannels (IrcMessage msg)
		{
			if ((Session as IrcSession).ChannelList == null)
			{
				Log.Error ("Channel List is NULL when we got End of Channel message!");
				return;
			}
			
			(_session as IrcSession).EmitChannelListReceived (new EventArgs ());
		}
		
		protected internal void HandleNames (IrcMessage msg)
		{
			OnMessageReceived(msg, "Members visible in ["+msg.Parameters[2]+"]: "+msg.LastParameter);
			
			if (msg.Parameters.Length >= 4)
			{
				//fill the conversation with contacts
				//"<nick> = <channel> :[[@|+]<nick> [[@|+]<nick> [...]]]"
				IrcSession session = Session as IrcSession;
				IrcChannel channel = session.ObtainChannel (msg[2], true);
				IrcConversation conv = session.ObtainConversation (channel, false);
				
				channel.Presence = IrcPresence.Online;
				
				string[] names = msg[3].Trim().Split (' ');
				
				foreach (string fullname in names)
				{
					if (String.IsNullOrEmpty (fullname))
						continue;
					
					char mode = fullname[0];
					string name = null;
					ChannelModeFlags flags = IrcProtocolHelper.GetChannelUserModeFlagsFromToken (mode);
					
					if (flags != ChannelModeFlags.None)
						name = fullname.Substring (1);
					else
						name = fullname;
					
					// Look for the contact in our collections
					IrcContact contact = session.ObtainContact (name, name);
					contact.ChannelUserMode = flags;
					
					if (conv.GetContact (name) == null)
					{
						conv.AddContact (contact);
						conv.EmitChannelContactInitialJoin (new ChannelContactEventArgs (channel, contact));
					}
					else
					{
						contact = conv.GetContact (name);
						//conv.EmitChannelContactModeChanged(new ChannelContactModeEventArgs (channel, contact, contact, IrcProtocolHelper.GetChannelModeString(flags)));
					}
				}
			}
			else
			{
				Log.Error ("Missing parameters in irc message for handlenames function!");
			}
		}

		protected internal void HandleEndOfNames (IrcMessage msg)
		{
			IrcSession session = Session as IrcSession;
			IrcChannel channel = session.ObtainChannel (msg[1], true);
			IrcConversation conv = session.ObtainConversation (channel, false);
			
			if (conv == null)
			{
				// Maybe we just listed the names, and we dont really care because we aren't part of the channel?
				return;
			}
			
			conv.IsInitialized = true;
			
			// Now that we have initialized a channel, we should find out more about it.
			session.GetMode (channel.DisplayName);
		}
		
		protected internal void HandleChannelModeIs (IrcMessage msg)
		{
			IrcSession session = Session as IrcSession;
			IrcChannel channel = session.ObtainChannel (msg.Parameters[1], false);
			IrcContact contact = session.ObtainContact (msg.Parameters[0], msg.Parameters[0]);
			
			if (channel != null)
			{
				ChannelModeFlags[] flags = null;
				bool give = false;
				
				if (IrcProtocolHelper.ParseChannelModeFlags (msg.Parameters[2], out give, out flags))
				{
					foreach (ChannelModeFlags flag in flags)
					{
						if (give)
							channel.ChannelFlags |= flag;
						else
							channel.ChannelFlags &= ~flag;
						
						//channel.EmitChannelModeChanged (new ChannelModeEventArgs (channel, contact, flag, give, null));
					}
				}
			}
		}
		
		protected internal void HandleNickname (IrcMessage msg)
		{
			// Somebody changed his/her name

			string oldName = msg.Identifier.Nickname;
			string newName = msg[0];
			
			if (oldName.ToLower().CompareTo (Session.Account.DisplayName.ToLower()) == 0)
			{
				// We changed our own nickname
				(Session.Account as IrcAccount).SetDisplayName(newName);
				OnMessageReceived(msg, "Successfully changed nickname to ["+msg.LastParameter+"].");
			}
			
			IrcSession session = Session as IrcSession;
			
			// Change all contacts inside each channel
			foreach (IContact cycle in Session.ContactCollection)
			{
				IrcChannel channel = cycle as IrcChannel;
				
				if (channel == null)
					continue;
				
				if (channel.Presence == IrcPresence.Offline)
					continue;
				
				IContact contact = channel.ContactCollection.GetContactByName (oldName);
				
				if (contact != null)
				{
					contact.DisplayName = newName;
					session.EmitContactNameChanged (new EntityChangeEventArgs <string> (contact, newName, oldName));
				}
			}
			
			// Contact can also be in the global contact list
			IContact globalContact = Session.ContactCollection.GetContactByName (oldName);
			if (globalContact != null)
			{
				globalContact.DisplayName = newName;
				session.EmitContactNameChanged (new EntityChangeEventArgs <string> (globalContact, newName, oldName));
			}
		}
		
		protected internal void HandlePing (IrcMessage msg)
		{
			//pong with the given reply string
			Pong (msg.Parameters[0]);
		}
		
		protected internal void HandleServerQuit (IrcMessage msg)
		{
			Disconnect ();
		}
		
		protected internal void HandleTopic (IrcMessage msg)
		{
			//the topic of a channel was changed
			//:<contact> TOPIC <channel> :<topic>
			IrcSession session = Session as IrcSession;
			IrcChannel channel = session.ObtainChannel (msg[0], true);
			channel.Topic = msg[1];
			
			//TODO: emit an event so the person who changed the topic is known as well
		}
		
		protected internal void HandleInitialTopic (IrcMessage msg)
		{
			//:<server> 332 <me> <channel> :<topic>
			IrcSession session = Session as IrcSession;
			IrcChannel channel = session.ObtainChannel (msg[1], true);
			channel.SetTopic(msg[2]);
			
			//TODO: emit a session event
		}
		
		protected internal void HandleErrorCannotSendToChannel (IrcMessage msg)
		{
			IrcSession session = Session as IrcSession;
			
			IrcChannel channel = session.ObtainChannel(msg[1], false);
			
			if (channel != null)
			{
				channel.EmitCannotSend (new ChannelEventArgs (channel));
			}
			else
			{
				Log.Warn ("Received an error for a channel that doesn't exist: "+msg[1]);
			}
		}
		
		protected internal void HandleError (IrcMessage msg)
		{
			IrcSession session = Session as IrcSession;
			
			if (msg.LastParameter.IndexOf ("Closing Link") < 0)
				session.EmitErrorOccurred (new ErrorEventArgs (session, msg.LastParameter, "Connection Error!"));
		}
		
		protected internal void HandleStartBanList (IrcMessage msg)
		{
			IrcSession session = Session as IrcSession;
			IrcChannel channel = session.ObtainChannel (msg.Parameters[1], true);
			
			if (channel != null)
			{
				channel.BanMasks.Clear ();
			}
		}
		
		private List<string> _banMasks = new List<string> ();
		
		protected internal void HandleBanList (IrcMessage msg)
		{
			string mask = msg.Parameters[2];
			string server = msg.Parameters[3];
			
			IrcSession session = Session as IrcSession;
			IrcChannel channel = session.ObtainChannel (msg.Parameters[1], true);
			
			if (channel != null)
			{
				if (!_banMasks.Contains (mask))
					_banMasks.Add (mask);
			}
		}
		
		protected internal void HandleEndBanList (IrcMessage msg)
		{
			IrcSession session = Session as IrcSession;
			IrcChannel channel = session.ObtainChannel (msg.Parameters[1], true);
			
			if (channel != null)
			{
				channel.BanMasks.Clear();
				
				foreach (string mask in _banMasks)
				{
					channel.BanMasks.Add (mask);
				}
				
				_banMasks.Clear ();
				
				channel.EmitChannelBanListChanged(new ChannelEventArgs (channel));
			}
		}
		
		protected internal void HandleBannedFromChannel (IrcMessage msg)
		{
			IrcChannel channel = Session.ContactCollection.GetContact (msg.Parameters[1]) as IrcChannel;
			IrcConversation conv = Session.Conversations.GetConversation (channel) as IrcConversation;
			
			OnMessageReceived(msg, "You are banned from ["+msg.Parameters[1]+"]. Unable to join!");
			
			if (conv != null)
				conv.EmitChannelBanned ();
		}
		
		protected internal void HandleInviteOnlyChannel (IrcMessage msg)
		{
			IrcChannel channel = Session.ContactCollection.GetContact (msg.Parameters[1]) as IrcChannel;
			IrcConversation conv = Session.Conversations.GetConversation (channel) as IrcConversation;
			
			OnMessageReceived(msg, "You have not been invited to ["+msg.Parameters[1]+"]. Unable to join!");
			
			if (conv != null)
				conv.EmitChannelInviteOnly ();
		}
		
		protected internal void HandleMotd (IrcMessage msg)
		{
			OnMessageReceived (msg, msg.LastParameter);
		}
		
		protected internal void HandleConnectionEstablished ()
		{
			IrcSession session = Session as IrcSession;
			
			session.EmitLoginCompleted (new SessionEventArgs (session));
			
			session.SetAwayPresence (Session.Account.InitialPresence);
			session.Account.EmitPresenceChange (new EntityChangeEventArgs<IPresence> (session.Account, session.Account.InitialPresence, IrcPresence.Offline));
			
			_isLoggedIn = true;
			// Also I think that it would be wise to WHOIS all contacts/channels for details
			
		}
		
		protected internal void HandleNicknameInUse (IrcMessage msg)
		{
			if (!_isLoggedIn)
			{
				//TODO: instead of an error, allow the user to pick another nickname
				IrcSession session = Session as IrcSession;
				
				base.Disconnect ();
				
				session.EmitErrorOccurred (new ErrorEventArgs (session, "Nickname is already in use.", "Nickname Already In Use!"));
			}
			else
			{
				OnMessageReceived(msg, "Nickname ["+msg.Parameters[1]+"] is already in use. Unable to change!");
				// We attempted to change nickname after logging in, and it failed.
			}
		}
		
		protected internal void HandleHeavyServerLoad (IrcMessage msg)
		{
			if (!_isLoggedIn)
			{
				IrcSession session = Session as IrcSession;
				
				session.EmitErrorOccurred (new ErrorEventArgs (session, "Heavy server load.", "Heavy server load. Please wait a while and try again."));
			}
			else
			{
				OnMessageReceived(msg, "Heavy server load. Please wait a while and try again.");
			}
		}
		
		protected internal void HandleDcc (IrcContactIdentifier identifier, string datastr)
		{
			string[] data = TextUtility.SplitStringOnSpace (datastr, true, false);
			
			IrcSession session = Session as IrcSession;
			IrcContact contact = session.ObtainContact (identifier.Nickname, identifier.Hostname);
			
			if (data.Length <= 1)
			{
				Log.Warn ("There was no data passed during this DCC");
				return;
			}
			
			switch (data[1].ToUpper ())
			{
				case IrcConstants.Commands.DccChat:
					//DCC CHAT <protocol> <ip> <port>
					if (data.Length < 5)
					{
						Log.Debug ("Malformed DCC Send Request!");
						break;
					}
					
					string protocol = data[2];
					if (protocol.ToLower().CompareTo ("chat") != 0)
					{
						Log.Warn ("Attempted non-standard DCC chat protocol! Not implemented: "+protocol);
						break;
					}
					
					string ip = IrcProtocolHelper.ConvertLongToIPAddress (data[3]);
					if (ip == null)
					{
						Log.Warn ("Malformed DCC Send Request!");
						break;
					}
					
					int port;
					if (!int.TryParse (data[4], out port))
						break;
					
					// Lets get a conversation object to use for this chat
					IrcConversation conv = session.ObtainConversation (contact, true);
					
					// Create and attach a DCC connection to the conversation
					DccConnectionInfo connectInfo = new DccConnectionInfo (true, ip, port);
					DccChatConnection chat = new DccChatConnection (session, conv, connectInfo);
					conv.ChatConnection = chat;
					
					// Connect the DCC connection
					chat.Connect ();
					break;
					
				case IrcConstants.Commands.DccSend:
					//DCC SEND <filename> <ip in network order> <port> <file size> --> normal dcc
					//DCC SEND <filename> <ip in network order> 0 <filesize> <token> --> reverse/firewall dcc
					//filename can be encapsulated in quotes if it contains a space
					
					if (data.Length < 5)
					{
						Log.Debug ("Malformed DCC Send Request!");
						break;
					}
					
					string filename = data[2];
					filename = IrcProtocolHelper.DecodeDccFilename (filename);
					
					ip = IrcProtocolHelper.ConvertLongToIPAddress (data[3]);
					if (ip == null) //somebody is trying to send us broken data
					{
						Log.Warn ("Malformed DCC Send Request!");
						break;
					}
					
					if (!int.TryParse (data[4], out port))
						break;
					
					long filesize = 0;
					if (data.Length == 6)
					{
						if (!long.TryParse (data[5], out filesize))
						{
							Log.Debug ("Malformed DCC send request!");
							break;
						}
					}
					
					IrcFileTransfer transfer = null;
	
					string token = null;
					if (port == 0) {
						token = data[6];
						
						//TODO: if a transfer with the same token exists, then this is the accept message
					}
					
					transfer = new IrcFileTransfer (true, session, contact, ip, port, filename, filesize, token);
					session.AllFileTransfers.Add (transfer);
					
					session.EmitTransferInvitationReceived (new FileTransferEventArgs (transfer));
					
					ActivityUtility.EmitActivity (this, new ReceivedFileActivity (session, transfer));
					
					FileTransferUtility.Add (transfer);
					
					//TODO: a transfer request dialog should pop up with the options: cancel, accept, resume (if applicable)
					//TODO: send RESUME <offset> if needed + set the offset
					break;
					
				case IrcConstants.Commands.DccAccept:
					Log.Debug ("Received a DCC Accept request!");
					//a DCC RESUME request is granted
					//DCC ACCEPT <filename> <port> <position_in_file> [<token> if port=0]
					
					if (data.Length < 5)
						break;
					
					filename = data[2];
					
					//int port;
					if (!int.TryParse (data[3], out port))
						break;
					
					token = null;
					if (port == 0)
						token = data[5];
					
					IFileTransfer fileTransfer = GetFileTransfer (contact, filename, port, token);
					if (fileTransfer != null)
						fileTransfer.Accept ();
					
					break;
					
				case IrcConstants.Commands.DccResume:
					Log.Debug ("Received a DCC Resume request!");
					
					//a contact request to start on a given position for a file transfer that we initiated (or reverse dcc)
					//DCC RESUME <filename> <port> <position_in_file> [<token> if port=0]
	
					if (data.Length < 5)
						break;
					
					filename = data[2];
					
					//int port;
					if (!int.TryParse (data[3], out port))
						break;
					
					long resumeOffset;
					if (!long.TryParse (data[4], out resumeOffset))
						break;
					
					token = null;
					if (port == 0)
						token = data[5];
					
					fileTransfer = GetFileTransfer (contact, filename, port, token) as IrcFileTransfer;
					if (fileTransfer == null)
						break;
					
					(fileTransfer as IrcFileTransfer).SetTransferedBytes (resumeOffset);
					(fileTransfer as IrcFileTransfer).SendResumeReply ();
					
					break;
			}
		}
		
		
		protected internal void HandleCtcpResponse (IrcContactIdentifier contact, string datastr)
		{
			//TODO: implement
		}

		protected internal void HandleCtcpRequest (IrcContactIdentifier contact, string datastr)
		{
			string[] data = datastr.Split (' ');

			StringBuilder ctcp = new StringBuilder ();
			
			switch (data[0]) {
			case IrcConstants.Commands.CtcpClientInfo:
				ctcp.Append (IrcConstants.Commands.CtcpClientInfo);
				ctcp.Append (' ');
				ctcp.Append ("Supported CTCP commands: ClientInfo, Finger, Ping, Source, Time, UserInfo and Version");
				break;
			case IrcConstants.Commands.CtcpErrorMessage:
				//TODO: handle message
				return;
			case IrcConstants.Commands.CtcpUserInfo:
			case IrcConstants.Commands.CtcpFinger:
				//TODO: this is a user defined string
				return;
			case IrcConstants.Commands.CtcpPing:
				ctcp.Append (IrcConstants.Commands.CtcpPing);
				ctcp.Append (' ');
				ctcp.Append (data[1]);
				break;
			case IrcConstants.Commands.CtcpSource:
				ctcp.Append (ClientUtility.ClientWebsite);
				break;
			case IrcConstants.Commands.CtcpTime:
				//TODO: implement
				return;
			case IrcConstants.Commands.CtcpVersion:
				ctcp.Append (ClientUtility.ClientName);
				ctcp.Append (' ');
				ctcp.Append (ClientUtility.ClientVersion);
				break;
			default:
				return;
			}
			
			StringBuilder sb = new StringBuilder ();
			sb.Append (IrcConstants.Commands.Notice);
			sb.Append (" :");
			sb.Append (IrcConstants.Markers.Ctcp);
			sb.Append (IrcConstants.Commands.Ctcp);
			sb.Append (' ');
			sb.Append (contact.Nickname);
			sb.Append (' ');
			
			sb.Append (ctcp.ToString ());
			
			sb.Append (IrcConstants.Markers.Ctcp);
			Send (sb);
		}
		
		protected internal IFileTransfer GetFileTransfer (IrcContact contact, string encodedFilename, int port, string token)
		{
			foreach (IrcFileTransfer transfer in Session.AllFileTransfers) {
				if (transfer.EncodedFileName != encodedFilename)
					continue;
				if (transfer.Contact.DisplayName != contact.DisplayName)
					continue;
				
				if (port == 0) {
					if (transfer.DccConnectionInfo.ReverseToken != token)
						continue;
				} else {
					if (transfer.DccConnectionInfo.Port != port)
						continue;
				}

				return transfer;
			}
			return null;
		}
	}
}